-
Notifications
You must be signed in to change notification settings - Fork 116
Fix Invalid Pending Payments and Strengthen RBF Tests #647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix Invalid Pending Payments and Strengthen RBF Tests #647
Conversation
I've assigned @tnull as a reviewer! |
🔔 1st Reminder Hey @tnull! This PR has been waiting for your review. |
src/wallet/mod.rs
Outdated
} else { | ||
PaymentStatus::Pending | ||
}; | ||
if payment_status == PaymentStatus::Pending { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR. Since PaymentStatus::Pending
is only assigned in the else branch above, we can move the unconfirmed_txid.push(id)
call directly into that block. This avoids the extra comparison afterward and makes the logic a bit more concise and self-contained.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Thanks
56a50c1
to
569cebe
Compare
🔔 2nd Reminder Hey @tnull! This PR has been waiting for your review. |
🔔 3rd Reminder Hey @tnull! This PR has been waiting for your review. |
🔔 4th Reminder Hey @tnull! This PR has been waiting for your review. |
🔔 5th Reminder Hey @tnull! This PR has been waiting for your review. |
569cebe
to
c635cb4
Compare
🔔 6th Reminder Hey @tnull! This PR has been waiting for your review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excuse the delay here!
tests/common/mod.rs
Outdated
(node_a, node_b) | ||
} | ||
|
||
pub(crate) fn new_node(chain_source: &TestChainSource, anchor_channels: bool) -> TestNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change from the config_node
macro to this method makes sense to me, but maybe we want to align names here?
Should this rather be renamed to setup_node
, while renaming the current setup_node
to setup_node_from_config
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree with you. I’ve made the change.
tests/common/mod.rs
Outdated
use bitcoin::hashes::hex::FromHex; | ||
use bitcoin::hashes::sha256::Hash as Sha256; | ||
use bitcoin::hashes::Hash; | ||
use bitcoin::hashes::{hex::FromHex, Hash}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Please group imports at the module level.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
tests/common/mod.rs
Outdated
let address = bitcoind.new_address().expect("failed to get new address"); | ||
|
||
let request_block_template = | ||
corepc_node::TemplateRequest { rules: vec![electrsd::corepc_node::TemplateRules::Segwit] }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Please avoid the full type qualifiers, rather import the types via use
above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
node | ||
} | ||
|
||
pub(crate) fn generate_block_and_insert_transactions<E: ElectrumApi>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if it would be simpler to
- Put the RBF into the mempool
- Sync the wallet so it registers
- Mine a block
- Call Bitcoin Core's
invalidateblock
to discard the tip and the RBF - Put the original TX into the mempool
- Sync the wallet again
If I'm not mistaken that would save us all of this boilerplate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That approach doesn’t seem to work. After the block is mined and then invalidated, the replaced transaction isn’t discarded. I tested this flow, and when the original transaction is reintroduced, it fails because the existing transaction has a higher fee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That approach doesn’t seem to work. After the block is mined and then invalidated, the replaced transaction isn’t discarded. I tested this flow, and when the original transaction is reintroduced, it fails because the existing transaction has a higher fee.
Interesting, I was under the impression that invalidateblock
doesn't put the transactions back into the mempool but would simply discard them. It's likely not working because you use the bitcoind wallet to generate and RBF the transaction, having it track as part of the wallet (rather than 'just' in the mempool).
In any case, the block mining boilerplate is probably fine here then, even though I had hoped it could be simplified.
let id = failed_payment.id; | ||
let payment_update = PaymentDetailsUpdate { | ||
id, | ||
status: Some(PaymentStatus::Failed), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, not convinced we should do this, as on a semantic level there is no such thing as a 'failed' onchain payment. Valid transactions are forever pending as they could always be rebroadcasted even after being evicted from the local mempool. That is why we currently only track them as Pending
or Confirmed
(once they are irrevocably confirmed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your point. When a transaction leaves the mempool due to a low fee, it can still be valid and eventually confirmed. The issue arises with RBF, since only one of the conflicting transactions can be confirmed, making the others effectively double-spent and thus “failed”. The challenge is that BDK doesn’t currently provide a way to distinguish between these cases.
My main motivation is that keeping transactions evicted from the mempool marked as Pending
can lead to UX confusion, especially with RBF. For example, if a user broadcasts a transaction and then later replaces it via RBF, they would see two onchain payments, one that gets confirmed and another that never will. Leaving the latter as Pending would incorrectly suggest it’s still valid. Maybe having an additional status like Dropped
or Evicted
could better represent transactions that are no longer canonical in BDK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The challenge is that BDK doesn’t currently provide a way to distinguish between these cases.
I think the new WalletEvent::TxReplaced
should allow us to distinguish the cases and track all 'failed'/RBF'd transactions as part of the same payment entry? That should be tackled as part of #658, IMO.
My main motivation is that keeping transactions evicted from the mempool marked as
Pending
can lead to UX confusion, especially with RBF. For example, if a user broadcasts a transaction and then later replaces it via RBF, they would see two onchain payments, one that gets confirmed and another that never will. Leaving the latter as Pending would incorrectly suggest it’s still valid. Maybe having an additional status likeDropped
orEvicted
could better represent transactions that are no longer canonical in BDK?
Right, that's basically #452 which should be addressed by #658. Apart from that, leaving non-RBF'd (but evicted) transactions as Pending
is closer to reality and hence IMO preferable as long as we're not 100% they could eventually return and get mined.
…nfig Introduce setup_node for clearer node creation. Rename old setup_node to setup_node_from_config for custom configs.
…testing - Introduce `generate_block_and_insert_transactions` to manually mine a block with arbitrary transactions. - Update `bump_fee_and_broadcast` to optionally insert transactions directly into a block (bypassing the mempool) when `is_insert_block` is true. - Add integration test to insert the original (pre-RBF) transaction into a block instead of the RBF, to cover scenarios where the original transaction is confirmed and the RBF is not.
This test simulates multiple RBF transactions, randomly confirms one, then simulates a reorg and confirms another at random. This ensures the wallet correctly handles cases where any RBF transaction may be confirmed after
…oval Pending payments that become invalid or are removed from the mempool are now marked as failed. RBF tests were updated to check that only confirmed transactions succeed and all others are failed.only the actually confirmed transactions have the correct
c635cb4
to
ab6990e
Compare
Pending payments that are invalidated or removed from the mempool are now marked as failed. Currently, “failed” simply means the transaction is no longer in BDK’s canonical transaction list while still pending in our DB. It could later return to a pending or confirmed state if it reappears. More precise context for these “failed” states will be possible once bdk_wallet#257 is merged, as that adds richer uncanonica transaction status details.
Additional improvements:
generate_block_and_insert_transactions
to mine blocks with chosen transactions, enabling tests where either the original or a replacement transaction is confirmed.new_node
helper for clarity.Note: This PR partially addresses issue #452. I believe it does not conflict with PR #628, as this change is more general. It handles any transaction that is no longer canonical in BDK, not just RBF replacements. This fixes the problem where such transactions would remain pending indefinitely due to lack of further updates, by marking them as failed. PR #628, on the other hand, specifically handles RBFs initiated by the node.